T001 自定义控件 蜘蛛网状图

参考知识点:01.2 精通自定义 View 之绘图基础——路径

从效果图中可以看出,我们要先画出一个网格,默认网格数和边角数都是 6。在代码中,为了简化逻辑,我们会将所有可变的内容,比如画笔颜色、网格数、边角数设为固定值。其实这些值都应该在初始化的时候通过对应的 set 函数设置到自定义控件内部,大家可以自行补充。

一、初始化

不要在 onDraw() 函数中创建变量,所以必然会有一个初始化函数,用于在创建控件的时候初始化画笔等参数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
public class SpiderView extends View {
private Paint mRadarPaint; // 蜘蛛网
private Paint mRadarLinePaint; // 蜘蛛网辐射的六根线
private Paint mValuePaint; // 数据
private int mRadarPaintColor = 0xFF0099CC; // 网格默认颜色
private Path mPath;
public SpiderView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
init();
}
/**
* init view
* Paint、Path
*/
private void init() {
mRadarPaint = generatePaint(mRadarPaintColor, Paint.Style.FILL);
mRadarLinePaint = generatePaint(Color.WHITE, Paint.Style.STROKE);
mValuePaint = generatePaint(0xAFFF0000, Paint.Style.FILL);
mPath = new Path();
}
/**
* 初始化画笔
* @param color 画笔颜色
* @param style 画笔样式
* @return Paint
*/
private Paint generatePaint(int color, Paint.Style style) {
Paint paint = new Paint();
paint.setColor(color);
paint.setStyle(style);
paint.setAntiAlias(true);
return paint;
}
}

这里初始化了三个画笔,其中 mRadarPaint 是用来绘制蜘蛛网格的,类型设置为填充 (也可以设置为描边);mRadarLinePaint 是用来绘制蜘蛛网格辐射的六根线,白色描边;而 mValuePaint 是用来绘制结果图的,所以设置成带透明的红色画笔,样式为填充。

二、获得布局中心

在 onSizeChanged(int w, int h, int oldW, int oldH) 函数中,根据 View 的长、宽,获取整个布局的中心坐标,因为整个雷达都是从这个中心坐标开始绘制的。

1
2
3
4
5
6
7
8
9
10
11
12
13
private float radius; // 网格最大半径
private int centerX; // 中心 X
private int centerY; // 中心 Y
@Override
protected void onSizeChanged(int w, int h, int oldW, int oldH) {
// 获得布局中心
centerX = w / 2;
centerY = h / 2;
radius = Math.min(w, h) / 2f * 0.8f;
postInvalidate();
super.onSizeChanged(w, h, oldW, oldH);
}

我们知道,在控件大小发生变化时,都会通过 onSizeChanged() 函数通知我们当前控件的大小。所以,我们只需要重写 onSizeChanged() 函数,即可得知当前控件的最新大小。

为了不顶边,将蜘蛛网的半径设置为 Math.min(w, h) / 2f * 0.8f 。

然后依据绘图中心,分别绘制蜘蛛网格、网格中线、数据图,即可完成整个效果图的绘制。

1
2
3
4
5
6
7
8
9
10
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
// 绘制蜘蛛网格
drawPolygon(canvas);
// 绘制中线
drawLines(canvas);
// 画数据图
drawRegion(canvas);
}

三、绘制蜘蛛网格

下面我们就要绘制蜘蛛网格了,效果如下图所示。

效果图

很显然,蜘蛛网格是利用 Path 的 moveTo() 和 lineTo() 函数一圈圈画出来的,我们需要计算出每个转折点的位置。比如,计算下图中所标记点的 x, y 坐标。

很明显,标记点在半径的 3/4 位置,而标记点与中心点的连线与 X 轴的夹角为 a,所以由图可得:

1
2
x = centX + 3/4 * radius * sina;
y = centY + 3/4 * radius * cosa;

因为我们共画了 6 个角,所以每个角的度数应该是 360°/6 = 60°。
依据上面的原理,列出画蜘蛛网格的代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
private int count = 6; // 多边形,默认值为 6
private double angle = 2 * Math.PI / count; // 角度,值为 2π / count,默认
private int maxValue = 4; // 最大值
private void drawPolygon(Canvas canvas) {
float r = radius / maxValue; // r是蜘蛛丝之间的间距
for (int i = 1; i <= maxValue; i++) { // 中心点不用绘制
float curR = r * i; // 当前半径
mPath.reset();
for (int j = 0; j < count; j++) {
if (j == 0) {
mPath.moveTo(centerX + curR, centerY);
} else {
// 根据半径,计算出蜘蛛丝上每个点的坐标
float x = (float) (centerX + curR * Math.cos(angle * j));
float y = (float) (centerY + curR * Math.sin(angle * j));
mPath.lineTo(x, y);
}
}
mPath.close(); // 闭合路径
mRadarPaint.setAlpha(getRadarPaintColor(i));
canvas.drawPath(mPath, mRadarPaint);
}
}

四、画网格中线

在画完蜘蛛网格以后,我们需要画从网格中心到末端的直线,代码如下:

1
2
3
4
5
6
7
8
9
10
private void drawLines(Canvas canvas) {
for (int i = 0; i < count; i++) {
mPath.reset();
mPath.moveTo(centerX, centerY);
float x = (float) (centerX + radius * Math.cos(angle * i));
float y = (float) (centerY + radius * Math.sin(angle * i));
mPath.lineTo(x, y);
canvas.drawPath(mPath, mRadarLinePaint);
}
}

网格中线效果图

绘制原理与绘制蜘蛛网格是一样的,先找到各个末端点的坐标,然后画一条从中心点到末端点的连线即可。

五、画数据图

绘制数据区域其实也很简单,首先要确定每个数据点的位置。当然,网格线中的每一层网格都应该对应一个数值,在这里为了方便起见,将网格的最大值设为 4,即每一层数值是按 1,2,3,4 分布的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
private int maxValue = 4; // 最大值
private double[] data = {2,3,1,3,4,3}; // 数据
private void drawRegion(Canvas canvas) {
mPath.reset();
for (int i = 0; i < count; i++) {
double percent = data[i] / maxValue;
float x = (float) (centerX + radius * Math.cos(angle * i) * percent);
float y = (float) (centerY + radius * Math.sin(angle * i) * percent);
if (i == 0) {
mPath.moveTo(x, centerY);
} else {
mPath.lineTo(x, y);
}
}
canvas.drawPath(mPath, mValuePaint);
}

效果图

六、完整代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
package com.xxt.xtest;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Path;
import android.util.AttributeSet;
import android.view.View;
import androidx.annotation.Nullable;
public class SpiderView extends View {
private Paint mRadarPaint; // 蜘蛛网
private Paint mRadarLinePaint; // 蜘蛛网辐射的六根线
private Paint mValuePaint; // 数据
private int mRadarPaintColor = 0xFF0099CC; // 网格默认颜色
private Path mPath;
private float radius; // 网格最大半径
private int centerX; // 中心 X
private int centerY; // 中心 Y
private int count = 6; // 多边形,默认值为 6
private double angle = 2 * Math.PI / count; // 角度,值为 2π / count,默认
private int maxValue = 4; // 最大值
private double[] data = {2,3,1,3,4,3}; // 数据
public SpiderView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
init();
}
/**
* init view
* Paint、Path
*/
private void init() {
mRadarPaint = generatePaint(mRadarPaintColor, Paint.Style.FILL);
mRadarLinePaint = generatePaint(Color.WHITE, Paint.Style.STROKE);
mValuePaint = generatePaint(0xAFFF0000, Paint.Style.FILL);
mPath = new Path();
}
@Override
protected void onSizeChanged(int w, int h, int oldW, int oldH) {
// 获得布局中心
centerX = w / 2;
centerY = h / 2;
radius = Math.min(w, h) / 2f * 0.8f;
postInvalidate();
super.onSizeChanged(w, h, oldW, oldH);
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
// 绘制蜘蛛网格
drawPolygon(canvas);
// 绘制中线
drawLines(canvas);
// 画数据图
drawRegion(canvas);
}
private void drawPolygon(Canvas canvas) {
float r = radius / maxValue; // r是蜘蛛丝之间的间距
for (int i = 1; i <= maxValue; i++) { // 中心点不用绘制
float curR = r * i; // 当前半径
mPath.reset();
for (int j = 0; j < count; j++) {
if (j == 0) {
mPath.moveTo(centerX + curR, centerY);
} else {
// 根据半径,计算出蜘蛛丝上每个点的坐标
float x = (float) (centerX + curR * Math.cos(angle * j));
float y = (float) (centerY + curR * Math.sin(angle * j));
mPath.lineTo(x, y);
}
}
mPath.close(); // 闭合路径
mRadarPaint.setAlpha(getRadarPaintColor(i));
canvas.drawPath(mPath, mRadarPaint);
}
}
private void drawLines(Canvas canvas) {
for (int i = 0; i < count; i++) {
mPath.reset();
mPath.moveTo(centerX, centerY);
float x = (float) (centerX + radius * Math.cos(angle * i));
float y = (float) (centerY + radius * Math.sin(angle * i));
mPath.lineTo(x, y);
canvas.drawPath(mPath, mRadarLinePaint);
}
}
private void drawRegion(Canvas canvas) {
mPath.reset();
for (int i = 0; i < count; i++) {
double percent = data[i] / maxValue;
float x = (float) (centerX + radius * Math.cos(angle * i) * percent);
float y = (float) (centerY + radius * Math.sin(angle * i) * percent);
if (i == 0) {
mPath.moveTo(x, centerY);
} else {
mPath.lineTo(x, y);
}
}
canvas.drawPath(mPath, mValuePaint);
}
/**
* 初始化画笔
* @param color 画笔颜色
* @param style 画笔样式
* @return Paint
*/
private Paint generatePaint(int color, Paint.Style style) {
Paint paint = new Paint();
paint.setColor(color);
paint.setStyle(style);
paint.setAntiAlias(true);
return paint;
}
/**
* 由内到外,增加透明度
* @param i 第几个网格,从中心点算起
* @return int alpha 值
*/
private int getRadarPaintColor(int i) {
if (i > count || i < 1) {
return 0xFF;
}
int alpha = Color.alpha(mRadarPaintColor);
int colorStep = alpha / (maxValue - 1) - 10;
alpha = alpha - colorStep * (i - 1);
return alpha;
}
}